/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.tools.adtui.swing

import com.intellij.openapi.util.SystemInfo
import it.unimi.dsi.fastutil.ints.IntArrayList
import java.awt.Component
import java.awt.KeyboardFocusManager
import java.awt.event.InputEvent
import java.awt.event.KeyEvent
import javax.swing.KeyStroke

/**
 * A fake keyboard device that can be used for holding down keys in tests.
 *
 * Do not instantiate directly - use [FakeUi.keyboard] instead.
 */
class FakeKeyboard {
  private val pressedKeys = IntArrayList()
  private var focusedComponent: Component? = null

  /**
   * Set (or clear) the component that will receive the key events. Note that if the focus is
   * `null`, you can still press/release keys but no events will be dispatched.
   */
  fun setFocus(focus: Component?) {
    focusedComponent = focus
  }

  fun isPressed(keyCode: Int): Boolean {
    return pressedKeys.contains(keyCode)
  }

  /**
   * Begins holding down the specified key. You may release it later using [release].
   * This method will not generate `KEY_TYPED` events. For now those must be generated using
   * [type].
   *
   * For the key event to be handled by its target component, it is sometimes necessary to call
   * [com.intellij.testFramework.PlatformTestUtil.dispatchAllEventsInIdeEventQueue].
   */
  fun press(keyCode: Int) {
    performDownKeyEvent(keyCode, KeyEvent.KEY_PRESSED)
  }

  fun release(keyCode: Int) {
    check(isPressed(keyCode)) { "Can't release key ${KeyEvent.getKeyText(keyCode)} as it's not pressed" }
    pressedKeys.rem(keyCode)
    // Dispatch AFTER removing the key from our list of pressed keys. If it is a modifier key, we
    // don't want it to included in "toModifiersCode" logic called by "dispatchKeyEvent".
    dispatchKeyEvent(KeyEvent.KEY_RELEASED, keyCode)
  }

  fun pressAndRelease(keyCode: Int) {
    press(keyCode)
    release(keyCode)
  }

  /**
   * Types the specified key. This is a convenience method for generating `KEY_TYPED` events.
   *
   * TODO: We should consider having these events be generated by [press], but as the mapping
   *       between key presses and typed characters isn't straightforward in some cases (the simplest being
   *       typing capital letters and a more complex example being the generation of e.g. chinese characters
   *       using an input method) this might be a significant undertaking to do correctly.
   */
  fun type(keyCode: Int) {
    performDownKeyEvent(keyCode, KeyEvent.KEY_TYPED)
  }

  /** Produces a key press sequence for the given keystroke. */
  fun hit(keyStroke: KeyStroke) {
    pressForModifiers(keyStroke.modifiers)
    pressAndRelease(keyStroke.keyCode)
    releaseForModifiers(keyStroke.modifiers)
  }

  private fun performDownKeyEvent(keyCode: Int, event: Int) {
    check(!isPressed(keyCode)) { "Can't press key ${KeyEvent.getKeyText(keyCode)} as it's already pressed" }
    if (event == KeyEvent.KEY_PRESSED) {
      pressedKeys.add(keyCode)
    }
    dispatchKeyEvent(event, keyCode)
  }

  fun toModifiersCode(): Int {
    var modifiers = 0
    if (pressedKeys.contains(KeyEvent.VK_ALT)) {
      modifiers = modifiers or InputEvent.ALT_DOWN_MASK
    }
    if (pressedKeys.contains(KeyEvent.VK_CONTROL)) {
      modifiers = modifiers or InputEvent.CTRL_DOWN_MASK
    }
    if (pressedKeys.contains(KeyEvent.VK_SHIFT)) {
      modifiers = modifiers or InputEvent.SHIFT_DOWN_MASK
    }
    if (pressedKeys.contains(KeyEvent.VK_META)) {
      modifiers = modifiers or InputEvent.META_DOWN_MASK
    }
    return modifiers
  }

  /** Presses keys corresponding to the given modifiers. */
  fun pressForModifiers(modifiers: Int) {
    if (modifiers and InputEvent.ALT_DOWN_MASK != 0) {
      press(KeyEvent.VK_ALT)
    }
    if (modifiers and InputEvent.SHIFT_DOWN_MASK != 0) {
      press(KeyEvent.VK_SHIFT)
    }
    if (modifiers and InputEvent.CTRL_DOWN_MASK != 0) {
      press(KeyEvent.VK_CONTROL)
    }
    if (modifiers and InputEvent.META_DOWN_MASK != 0) {
      press(KeyEvent.VK_META)
    }
  }

  /** Releases keys corresponding to the given modifiers. */
  fun releaseForModifiers(modifiers: Int) {
    if (modifiers and InputEvent.META_DOWN_MASK != 0) {
      release(KeyEvent.VK_META)
    }
    if (modifiers and InputEvent.CTRL_DOWN_MASK != 0) {
      release(KeyEvent.VK_CONTROL)
    }
    if (modifiers and InputEvent.SHIFT_DOWN_MASK != 0) {
      release(KeyEvent.VK_SHIFT)
    }
    if (modifiers and InputEvent.ALT_DOWN_MASK != 0) {
      release(KeyEvent.VK_ALT)
    }
  }

  private fun dispatchKeyEvent(eventType: Int, keyCode: Int) {
    val focusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager()
    val component = focusedComponent ?: focusManager.focusOwner ?: return
    val correctedKeyCode = if (eventType == KeyEvent.KEY_TYPED) KeyEvent.VK_UNDEFINED else keyCode
    val event = KeyEvent(component, eventType, System.nanoTime(), toModifiersCode(), correctedKeyCode, keyCode.toChar())

    // If you use focusedComponent.dispatchEvent(), the event goes through a flow which gives other
    // systems a chance to handle it first. The following approach bypasses the event queue and
    // sends the event to listeners, directly.
    KeyboardFocusManager.getCurrentKeyboardFocusManager().redispatchEvent(component, event)
  }

  companion object {
    @JvmField
    val MENU_KEY_CODE = if (SystemInfo.isMac) KeyEvent.VK_META else KeyEvent.VK_CONTROL
  }
}